/* Copyright (c) 2008 Google Inc.
*  Copyright (c) 2009 Pierre Henri Kuate.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*     http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using Jayrock.Json;
using JsonConvert = Jayrock.Json.Conversion.JsonConvert;
using org.opensocial.data;
using System.Collections.Generic;

namespace org.opensocial.client
{
    /// <summary> An object which exposes a number of static methods for parsing JSON strings
    /// returned from RESTful or JSON-RPC requests into appropriate objects.
    /// </summary>
    /// <author>  Jason Cooper
    /// </author>
    public class OpenSocialJsonParser
    {
        /// <summary> Parses the passed JSON string into an OpenSocialResponse object -- if the
        /// passed string represents a JSON array, each object in the array is added
        /// to the returned object keyed on its "id" property.
        /// 
        /// </summary>
        /// <param name="input">The complete JSON string returned from an OpenSocial container
        /// in response to a request for data
        /// </param>
        /// <throws>  JSONException </throws>
        public static OpenSocialResponse getResponse(string input)
        {
            if (!isJsonArray(input))
            {
                if (!isJsonObject(input))
                    throw new OpenSocialRequestException("Container returned a non-Json response: " + input);
                ThrowError((JsonObject)JsonConvert.Import(input), input);
            }
            // TODO: This checking doesn't intercept errors like:
            // [{"id":"people","error":{"message":"unauthorized: Get profile permission denied.","code":401}}]
            // Ref. getEntryArray()

            var responseArray = (JsonArray)JsonConvert.Import(input);
            var r = new OpenSocialResponse();

            for (int i = 0; i < responseArray.Count; i++)
            {
                JsonObject o = responseArray.GetObject(i);

                if (o.Contains("id"))
                {
                    string id = (string)o["id"];
                    r.addItem(id, escape(o.ToString()));
                }
                else if (o.Contains("moodId"))
                {
                    string moodId = (string)o["moodId"];
                    r.addItem(moodId, escape(o.ToString()));
                }
                else
                {
                    throw new OpenSocialRequestException("JsonObject object without id: " + o);
                }
            }
            return r;
        }

        private static void ThrowError(JsonObject errorObject, string input)
        {
            var errorCode = errorObject["code"];
            var errorMessage = errorObject["message"];

            if (errorCode != null && errorMessage != null)
            {
                throw new OpenSocialRequestException(
                    "Container returned error code " + errorCode + "; " + errorMessage);
            }
            //else
            {
                throw new OpenSocialRequestException("Container returned error response: " + input);
            }
        }

        /// <summary> Parses the passed JSON string into an OpenSocialResponse object -- if the
        /// passed string represents a JSON object, it is added to the returned
        /// object keyed on the passed ID.
        /// </summary>
        /// <param name="input">The complete JSON string returned from an OpenSocial container
        /// in response to a request for data
        /// </param>
        /// <param name="id">The string ID to tag the JSON object string with as it is added
        /// to the OpenSocialResponse object
        /// </param>
        /// <throws>  JSONException </throws>
        public static OpenSocialResponse getResponse(string input, string id)
        {
            if (isJsonObject(input) || input.CompareTo(string.Empty) == 0)
            {
                var r = new OpenSocialResponse();
                r.addItem(id, escape(input));
                return r;
            }
            if (isJsonArray(input))
            {
                return getResponse(input);
            }
            
            throw new OpenSocialRequestException("Container returned a non-Json response: " + input);
        }

        /// <summary> Transforms a raw JSON object string containing profile information for a
        /// single user into an Person instance with all profile details
        /// abstracted as OpenSocialField objects associated with the instance.
        /// </summary>
        /// <param name="input">The JSON object string to parse as an Person object
        /// </param>
        /// <throws>  OpenSocialRequestException  </throws>
        /// <throws>  JSONException  </throws>
        public static data.Person parseAsPerson(string input)
        {
            return parseAsObject<data.Person>(input, true, "person");
        }

        /// <summary> Transforms a raw JSON object string containing profile information for a
        /// group of users into a list of Person instances with all profile
        /// details abstracted as OpenSocialField objects associated with the
        /// instances. These instances are then added to a Java List which gets returned. 
        /// </summary>
        /// <param name="input">The JSON object string to parse as a List of Person objects
        /// </param>
        /// <param name="useRpc"></param>
        /// <throws>  OpenSocialRequestException  </throws>
        /// <throws>  JSONException  </throws>
        public static Response<Person> parseAsPersonCollection(string input, bool useRpc)
        {
            return parseAsCollection<Person>(input, useRpc, "person");
        }

        /// <summary> Transforms a raw JSON object string containing key-value pairs (i.e. App
        /// Data) for one or more users into a specialized OpenSocialObject instance
        /// with each key-value pair abstracted as OpenSocialField objects associated
        /// with the instance.
        /// </summary>
        /// <param name="input">The JSON object string to parse as an AppData object
        /// </param>
        /// <throws>  JSONException  </throws>
        /// <throws>  OpenSocialRequestException  </throws>
        public static Response<AppData> parseAsAppData(string input)
        {
            if (isEntryArray(input))
            {
                return parseAsCollection<AppData>(input, false, "userAppData");
            }
            else
            {
                Response<AppData> response = new Response<AppData>();
                response.Add(parseAsObject<AppData>(input, true));
                return response;
            }
        }

        /// <summary>
        /// Method to parse the input JSON string, build Activity Objects with
        /// the entry values and return a list of Activity objects
        /// </summary>
        public static Response<Activity> parseAsActivityCollection(string input, bool useRpc)
        {
            return parseAsCollection<Activity>(input, useRpc, "activity");
        }

        /// <summary> Inspects the passed object for one of several specific properties and, if
        /// found, returns that property as a JsonArray object. All valid response
        /// objects which contain a data collection (e.g. a collection of people)
        /// must have this property.
        /// </summary>
        /// <param name="root">JsonObject to query for the presence of the specific property
        /// </param>
        /// <throws>  OpenSocialRequestException if property is not found in the passed object </throws>
        /// <throws>  JSONException </throws>
        private static JsonArray getEntryArray(JsonObject root)
        {
            if (root.Contains("error"))
                ThrowError((JsonObject)root["error"], "");

            if (root.Contains("entry"))
                return (JsonArray)root["entry"];
            if (root.Contains("data"))
                return (JsonArray)((JsonObject)root["data"])["list"];

            throw new OpenSocialRequestException("Entry not found");
        }

        /// <summary> Inspects the passed object for one of several specific properties and, if
        /// found, returns that property as a JsonObject object. All valid response
        /// objects which encapsulate a single data item (e.g. a person) must have
        /// this property.
        /// </summary>
        /// <param name="root">JsonObject to query for the presence of the specific property
        /// </param>
        /// <throws>  OpenSocialRequestException if property is not found in the passed object </throws>
        /// <throws>  JSONException </throws>
        private static JsonObject getEntryObject(JsonObject root, string entryName)
        {
            if (null != entryName && root.Contains(entryName))
                return (JsonObject)root[entryName];
            if (root.Contains("data"))
                return (JsonObject)root["data"];
            if (root.Contains("entry"))
                return (JsonObject)root["entry"];

            // otherwise, there was no "entry", return the root as is
            return root;
        }

        public static T parseAsObject<T>(string input, bool useRpc)
            where T : data.OpenSocialObject
        {
            return parseAsObject<T>(input, useRpc, null);
        }

        public static T parseAsObject<T>(string input, bool useRpc, string entryName)
            where T : data.OpenSocialObject
        {
            if (input == null)
                throw new OpenSocialRequestException("Response item with given key not found");

            JsonObject root = (JsonObject)JsonConvert.Import(input);
            if (useRpc) // The response is wrapped to support batching
            {
                root = getEntryObject(root, entryName);
            }

            return (T)parseAsObject(root, typeof(T));
        }

        /// <summary> Calls a function to recursively iterates through the the properties of the
        /// passed JsonObject object and returns an equivalent OpenSocialObject with
        /// each property of the original object mapped to fields in the returned object.
        /// </summary>
        /// <param name="entryObject">Object-oriented representation of JSON response
        /// string which is transformed into and returned as an
        /// OpenSocialObject
        /// </param>
        /// <param name="clientClass">Class of object to return, either OpenSocialObject
        /// or a subclass
        /// </param>
        /// <throws>  JSONException </throws>
        private static OpenSocialObject parseAsObject(JsonObject entryObject, System.Type clientClass)
        {
            OpenSocialObject o = (OpenSocialObject)System.Activator.CreateInstance(clientClass);

            IDictionary<string, OpenSocialField> entryRepresentation = null;

            if (null != entryObject["appData"] && clientClass == typeof(AppData))
            {
                entryRepresentation = createAppDataObjectRepresentation(entryObject);
            }
            else
            {
                entryRepresentation = createObjectRepresentation(entryObject);
            }

            foreach (var e in entryRepresentation)
            {
                o.setField(e.Key, e.Value);

                // myspace specific hack, they don't return an ID, but do return a URI to the resource,
                // the ID can be parsed out of that
                if (e.Key == "statusLink" && !e.Value.IsComplex && e.Value.StringValue.Contains("myspace.com"))
                {
                    string id = e.Value.StringValue.Substring(e.Value.StringValue.LastIndexOf("/") + 1);
                    OpenSocialField id_field = new OpenSocialField(false);
                    id_field.addValue(id);
                    o.setField("id", id_field);
                }
            }

            return o;
        }

        public static data.Response<T> parseAsCollection<T>(string input, bool useRpc)
            where T : data.OpenSocialObject
        {
            return parseAsCollection<T>(input, useRpc, null);
        }

        public static Response<T> parseAsCollection<T>(string input, bool useRpc, string entryName)
            where T : OpenSocialObject
        {
            if (input == null)
                throw new OpenSocialRequestException("Response item with given key not found");

            JsonObject root = (JsonObject)JsonConvert.Import(input);

            JsonArray entries = getEntryArray(root);

            List<T> l = new List<T>(entries.Count);

            for (int i = 0; i < entries.Count; i++)
            {
                JsonObject entry = entries.GetObject(i);

                if (null != entryName && entry.Contains(entryName))
                {
                    entry = (JsonObject)entry[entryName];
                }

                T p = (T)parseAsObject(entry, typeof(T));

                l.Add(p);
            }
            
            Response<T> response = parseAsObject<Response<T>>(input, useRpc);
            response.setList(l);
            return response;
        }

        /// <summary> Recursively iterates through the properties of the passed JsonObject
        /// object and returns a Map of OpenSocialField objects keyed on Strings
        /// representing the property values and names respectively. Used for AppData
        /// </summary>
        /// <param name="o">Object-oriented representation of a JSON object which is
        /// transformed into and returned as a Map of OpenSocialField
        /// objects keyed on Strings
        /// </param>
        /// <throws>  JSONException </throws>
        private static IDictionary<string, data.OpenSocialField> createAppDataObjectRepresentation(JsonObject o)
        {
            var r = new Dictionary<string, data.OpenSocialField>();
            string userId = null;

            foreach (System.Collections.DictionaryEntry entry in o)
            {
                if (entry.Key.ToString() == "personId")
                {
                    userId = entry.Value.ToString();
                }
                else if (entry.Key.ToString() == "appData")
                {
                    if (entry.Value is JsonArray)
                    {
                        JsonArray p = (JsonArray)entry.Value;
                        JsonObject obj = new JsonObject();

                        for (int i = 0; i < p.Count; i++)
                        {
                            obj[((JsonObject)p[i])["key"].ToString()] = ((JsonObject)p[i])["value"].ToString();
                        }

                        OpenSocialField field = new OpenSocialField(true);
                        field.addValue(new OpenSocialObject(createObjectRepresentation(obj)));
                        r.Add(userId, field);
                    }
                    else // As a last resort we will add in the value as a string (even if it is a bool)
                    {
                        OpenSocialField field = new OpenSocialField(false);
                        field.addValue(unescape(entry.Value.ToString()));
                        r.Add(userId, field);
                    }
                }
            }

            return r;
        }

        /// <summary> Recursively iterates through the properties of the passed JsonObject
        /// object and returns a Map of OpenSocialField objects keyed on Strings
        /// representing the property values and names respectively.
        /// </summary>
        /// <param name="o">Object-oriented representation of a JSON object which is
        /// transformed into and returned as a Map of OpenSocialField
        /// objects keyed on Strings
        /// </param>
        /// <throws>  JSONException </throws>
        private static IDictionary<string, data.OpenSocialField> createObjectRepresentation(JsonObject o)
        {
            var r = new Dictionary<string, data.OpenSocialField>();

            foreach (System.Collections.DictionaryEntry entry in o)
            {
                if (entry.Value is JsonObject)
                {
                    var p = (JsonObject) entry.Value;
                    var field = new data.OpenSocialField(true);
                    field.addValue(new data.OpenSocialObject(createObjectRepresentation(p)));
                    r.Add((string)entry.Key, field);
                }
                else if (entry.Value is JsonArray)
                {
                    var p = (JsonArray) entry.Value;
                    System.Collections.Generic.List<object> values = createArrayRepresentation(p);
                    var field = new data.OpenSocialField(true);

                    foreach (object v in values)
                        field.addValue(v);

                    r.Add((string)entry.Key, field);
                }
                else // As a last resort we will add in the value as a string (even if it is a bool)
                {
                    var field = new data.OpenSocialField(false);
                    field.addValue(unescape(entry.Value.ToString()));
                    r.Add((string)entry.Key, field);
                }
            }

            return r;
        }

        /// <summary> Iterates through the objects in the passed JsonArray object, recursively
        /// transforms each as needed, and returns a List of Java objects.
        /// </summary>
        /// <param name="a">Object-oriented representation of a JSON array which is iterated
        /// through and returned as a List of Java objects
        /// </param>
        /// <throws>  JSONException </throws>
        private static System.Collections.Generic.List<object> createArrayRepresentation(JsonArray a)
        {
            var r = new System.Collections.Generic.List<object>(a.Count);

            for (int i = 0; i < a.Count; i++)
            {
                string member = a.GetString(i);

                if (member.Length <= 0)
                    continue;

                if (isJsonObject(member))
                {
                    JsonObject p = a.GetObject(i);
                    r.Add(new data.OpenSocialObject(createObjectRepresentation(p)));
                }
                else if (isJsonArray(member))
                {
                    JsonArray p = a.GetArray(i);
                    System.Collections.Generic.List<object> values = createArrayRepresentation(p);

                    foreach (object v in values)
                        r.Add(v);
                }
                else // As a last resort we will add in the value as a string
                {
                    r.Add(member);
                }
            }

            return r;
        }

        internal static bool isEntryArray(string str)
        {
            int entryIndex = str.IndexOf("entry");

            if (entryIndex >= 0)
            {
                int arrayIndex = str.Substring(entryIndex).IndexOf("[");

                if (arrayIndex < 0)
                {
                    return false;
                }

                int objectIndex = str.Substring(entryIndex).IndexOf("{");

                if (objectIndex < 0)
                {
                    return true;
                }

                return arrayIndex < objectIndex;
            }

            return false;
        }

        private static bool isJsonArray(string str)
        {
            if (!string.IsNullOrEmpty(str))
                return str[0] == '[';

            return false;
        }

        private static bool isJsonObject(string str)
        {
            if (!string.IsNullOrEmpty(str))
                return str[0] == '{';

            return false;
        }

        /// <summary> Escapes "{ and }" as "%7B and "%7D respectively to prevent parsing errors
        /// when property values begin with { or } tokens.
        /// </summary>
        /// <param name="input">String to escape
        /// </param>
        /// <returns> escaped String
        /// </returns>
        private static string escape(string input)
        {
            string output = input;

            output = output.Replace("\"\\{", "\"%7B");
            output = output.Replace("\\}\"", "%7D\"");

            return output;
        }

        /// <summary> Unescapes String objects previously returned from the escape method by
        /// substituting { and } for %7B and %7D respectively. Called after
        /// parsing to restore property values.
        /// </summary>
        /// <param name="input">String to unescape
        /// </param>
        /// <returns> unescaped String
        /// </returns>
        private static string unescape(string input)
        {
            string output = input;

            output = output.Replace("%7B", "{");
            output = output.Replace("%7D", "}");

            return output;
        }
    }
}